package net.chrisrichardson.ormunit.hibernate;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import junit.framework.Assert;
import net.chrisrichardson.ormunit.ClassMappingException;
import net.chrisrichardson.ormunit.IncorrectAccessTypeException;
import net.chrisrichardson.ormunit.NonPersistentPropertyException;
import net.chrisrichardson.ormunit.NonexistentPropertiesException;
import net.chrisrichardson.ormunit.UnmappedPropertiesException;

import org.hibernate.MappingException;
import org.hibernate.PropertyNotFoundException;
import org.hibernate.cfg.Configuration;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.List;
import org.hibernate.mapping.ManyToOne;
import org.hibernate.mapping.OneToMany;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.SimpleValue;
import org.hibernate.mapping.Value;

public class MappedClassChecker {

	private final AccessStrategy accessStrategy;

	private final Class type;

	private final PersistentClass classMapping;
	private Class mappedSuper;
	
	public MappedClassChecker(Class type, Configuration configuration,
			AccessStrategy accessStrategy) {
		this.type = type;
		this.accessStrategy = accessStrategy;
		this.classMapping = configuration.getClassMapping(type.getName());
		this.mappedSuper = getMappedSuper(type, configuration);
	}

	private Class getMappedSuper(Class type, Configuration configuration) {
		Class superType = type;
		while ((superType = superType.getSuperclass()) != Object.class) {
			if (configuration.getClassMapping(superType.getName()) != null)
				break;
		}
		return superType;
	}

	public void assertClassMapping(Class type, String tableName) {
		Assert.assertNotNull("No class mapping found for " + type.getName(),
				classMapping);
		if (!tableName.equalsIgnoreCase(classMapping.getTable().getName()))
			throw new ClassMappingException(type, tableName, classMapping
					.getTable().getName());
		assertHasConstructor();
	}

	private void assertHasConstructor() {
		try {
			Constructor[] constructors = type.getDeclaredConstructors();
			for (Constructor constructor : constructors) {
				if (constructor.getParameterTypes().length == 0) {
					// have a default constructor
					if ((constructor.getModifiers() & Modifier.PRIVATE) != 0)
						Assert.fail("Constructor should not be private: "
								+ type.getName());
					return;
				}
			}
		} catch (SecurityException e) {
			throw new RuntimeException(e);
		}
		Assert.fail("No default constructor required by Hibernate: "
				+ type.getName());

	}

	public static Set getPropertiesWithNonSubProps(Set propertyNames) {
		return PathUtil.getPropertiesWithNonSubProps(propertyNames);
	}

	private void walkComponentProperties(Iterator propertyIterator,
			Set unmappedPropertyNames) {
		for (Iterator it = propertyIterator; it.hasNext();) {
			Property property = (Property) it.next();
			String name = property.getName();
			if (property.getPropertyAccessorName() != null)
        assertAccessType(property);
			if (property.getValue() instanceof Component) {
				Component cv = (Component) property.getValue();
				assertPropertiesOfComponentPropertyMapped(name, cv,
						unmappedPropertyNames);
			} else if (isListOfComponents(property)) {
				List value = (List) property.getValue();
				Component cv = (Component) value.getElement();
				assertPropertiesOfComponentPropertyMapped(name, cv,
						unmappedPropertyNames);
			}
		}
	}

	private void assertPropertiesOfComponentPropertyMapped(String name,
			Component cv, Set unmappedPropertyNames) {

		Set unmappedPropertiesForThisComponent = PathUtil.getPathsStartingWith(
				name, unmappedPropertyNames);

		Set<String> unmappedDirectProperties = PathUtil
				.getRoots(unmappedPropertiesForThisComponent);
		assertAllDirectPropertiesOfComponentMapped(cv, unmappedDirectProperties);
		assertPropertiesExists(cv.getComponentClass(), unmappedDirectProperties);
		walkComponentProperties(cv.getPropertyIterator(),
				unmappedPropertiesForThisComponent);
	}

	private void assertAllDirectPropertiesOfComponentMapped(Component cv,
			Set fieldsToIgnore) {
		Set unmappedProperties = new HashSet();
		Set mappedProperties = new HashSet();
		for (String propertyName : accessStrategy.getPersistableProperties(cv
				.getComponentClass(), Object.class)) {
			try {
				cv.getProperty(propertyName);
				mappedProperties.add(propertyName);
			} catch (MappingException e) {
				unmappedProperties.add(propertyName);
			}
		}
		unmappedProperties.removeAll(fieldsToIgnore);
		if (!unmappedProperties.isEmpty())
			throw new UnmappedPropertiesException(cv.getComponentClass(),
					unmappedProperties);
		Set x = intersection(mappedProperties, fieldsToIgnore);
		if (!x.isEmpty()) {
			throw new NonPersistentPropertyException(cv.getComponentClass(), x);

		}
	}

	private Set intersection(Set mappedProperties, Set fieldsToIgnore) {
		Set result = new HashSet(mappedProperties);
		result.retainAll(fieldsToIgnore);
		return result;
	}


	public ComponentPropertyMapping getComponentPropertyMapping(String propertyName)
			throws MappingException {
		Property property = classMapping.getProperty(propertyName);
		Component value = (Component) property.getValue();
		return new ComponentPropertyMapping(property, value, accessStrategy);
	}

	public void assertAllPropertiesMapped()
			throws MappingException {
		assertAllPropertiesMappedExcept(Collections.EMPTY_SET);
	}

	public static Set<String> getRoots(Set mungedPropertyNames) {
		return PathUtil.getRoots(mungedPropertyNames);
	}

	private void assertPropertiesExists(Class type, Set propertyNames) {
		Set<String> persistableProperties = accessStrategy.getPersistableProperties(
				type, type == this.type ? mappedSuper : Object.class);
		if (!persistableProperties.containsAll(propertyNames)) {
			propertyNames.removeAll(persistableProperties);
			throw new NonexistentPropertiesException(type, propertyNames);
		}
	}

	private void assertNoOtherUnmappedProperties(Class type,
			Set nonPersistentProperties)
			throws MappingException {
		Property idProperty = classMapping.getIdentifierProperty();
		Set unmappedProperties = new HashSet();
		Set fieldsToIgnore = new HashSet(nonPersistentProperties);
		if (idProperty != null) {
			fieldsToIgnore.add(idProperty.getName());
		}
		for (Iterator it = accessStrategy.getPersistableProperties(type,
				type == this.type ? mappedSuper : Object.class).iterator(); it.hasNext();) {
			String propertyName = (String) it.next();
			if (!fieldsToIgnore.contains(propertyName))
				try {
					classMapping.getProperty(propertyName);
				} catch (MappingException e) {
					unmappedProperties.add(propertyName);
				}
		}
		if (!unmappedProperties.isEmpty())
			throw new UnmappedPropertiesException(type, unmappedProperties);
	}

	private boolean isListOfComponents(Property property) {
		return property.getValue() instanceof List
				&& ((List) property.getValue()).getElement() instanceof Component;
	}

	public void assertAllPropertiesMappedExcept(Set propertyNames) throws MappingException {
		assertHasConstructor();
		Iterator propertyIterator = classMapping.getPropertyIterator();
		walkComponentProperties(propertyIterator, propertyNames);

		assertNoMappingForProperties(type, PathUtil
				.getPropertiesWithNonSubProps(propertyNames));
		assertNoOtherUnmappedProperties(type, PathUtil.getRoots(propertyNames));
		assertPropertiesExists(type, PathUtil.getRoots(propertyNames));
	}

	private void assertNoMappingForProperties(Class type, Set propertyNames) {
		for (Iterator iter = propertyNames.iterator(); iter.hasNext();) {
			String propertyName = (String) iter.next();
			try {
				Property r = classMapping.getProperty(propertyName);
				if (r.getName().equals(propertyName))
					throw new NonPersistentPropertyException(type, Collections
							.singleton(propertyName));
			} catch (MappingException e) {

			}
		}
	}

	public void assertCompositeListProperty(String propertyName)
			throws MappingException {
		Property property = classMapping.getProperty(propertyName);
		HibernateAssertUtil.assertPropertyType(property, "<component-list>", org.hibernate.mapping.List.class);
	}

	public ListPropertyMapping assertListProperty(String propertyName)
			throws MappingException {
		Property property = classMapping.getProperty(propertyName);
		Value v = property.getValue();
    HibernateAssertUtil.assertPropertyType(property, "<list>", org.hibernate.mapping.List.class);
		org.hibernate.mapping.List value = (org.hibernate.mapping.List) v;
		return new ListPropertyMapping(property, value);
	}

	public CompositeListPropertyMapping getCompositeListPropertyMapping(
			String propertyName) throws MappingException {
		Property property = classMapping.getProperty(propertyName);
		Value v = property.getValue();
		org.hibernate.mapping.List value = (org.hibernate.mapping.List) v;
		return new CompositeListPropertyMapping(property, value, accessStrategy);
	}

	public ListPropertyMapping getListPropertyMapping(String propertyName)
			throws MappingException {
		Property property = classMapping.getProperty(propertyName);
		Value v = property.getValue();
		org.hibernate.mapping.List value = (org.hibernate.mapping.List) v;
		return new ListPropertyMapping(property, value);
	}

	public void assertComponentProperty(String propertyName)
			throws MappingException {
		Property property = classMapping.getProperty(propertyName);
    HibernateAssertUtil.assertPropertyType(property, "<component>", Component.class);
		if (!(property.getValue() instanceof Component))
			Assert.fail("field " + propertyName
					+ " is not mapped as a component: " + property.getValue());
	}

	public void assertSetProperty(String propertyName) throws MappingException {
		Property property = classMapping.getProperty(propertyName);
		org.hibernate.mapping.Set value = (org.hibernate.mapping.Set) property
				.getValue();
	}

	public ToOnePropertyMapping assertManyToOneProperty(String propertyName,
			String foreignKeyColumnName) throws MappingException {
		Property subProperty = classMapping.getProperty(propertyName);
		ManyToOne value = (ManyToOne) subProperty.getValue();
		HibernateAssertUtil.assertColumn(foreignKeyColumnName, value);
		return new ToOnePropertyMapping(subProperty, value);
	}

	public void assertVersionProperty(String propertyName, String columnName) {
		Property versionProperty = classMapping.getVersion();
		Assert.assertEquals(propertyName, versionProperty.getName());
		HibernateAssertUtil.assertPropertyColumn(columnName, versionProperty);
	}

	public void assertIdProperty(String idPropertyName, String idColumn)
			throws PropertyNotFoundException, MappingException {
		Property idProperty = classMapping.getIdentifierProperty();
		Assert.assertEquals("Incorrect id property name", idPropertyName, idProperty.getName());
		assertAccessType(idProperty);
	}

  private void assertAccessType(Property property) {
    String expected = accessStrategy.getName();
    String actual = property.getPropertyAccessorName();
    if (!expected.equals(actual))
      throw new IncorrectAccessTypeException(String.format("Property <%s.%s> uses wrong access type. Expected access=\"%s\" but got access=\"%s\"", property.getPersistentClass().getClassName(), property.getName(), expected, actual));
  }

	public void assertProperty(String propertyName, String columnName)
			throws MappingException {
		assertProperty(propertyName, columnName, null);
	}

	public void assertProperty(String propertyName, String columnName,
			Class fieldType) {
		Property subProperty = classMapping.getProperty(propertyName);
		HibernateAssertUtil.assertPropertyColumn(columnName, subProperty);
		if (fieldType != null) {
			Value value = subProperty.getValue();
			SimpleValue sv = (SimpleValue) value;
			Assert.assertEquals(fieldType.getName(), sv.getTypeName());
		}
		assertAccessType(subProperty);
	}

	public void assertOneToManyListProperty(String propertyName,
			String foreignKeyColumn, String indexColumn)
			throws MappingException {
		Property property = classMapping.getProperty(propertyName);
		org.hibernate.mapping.List value = (org.hibernate.mapping.List) property
				.getValue();
		HibernateAssertUtil.assertColumn(foreignKeyColumn, value.getKey());
		HibernateAssertUtil.assertColumn(indexColumn, value.getIndex());
		Assert.assertTrue(value.getElement() instanceof OneToMany);
	}

	public void assertAllPropertiesMappedExcept(String field1, String field2,
			String field3) throws MappingException {
		Set s = new HashSet();
		s.add(field1);
		s.add(field2);
		s.add(field3);
		assertAllPropertiesMappedExcept(s);
	}

	public void assertAllPropertiesMappedExcept(String field1, String field2)
			throws MappingException {
		Set s = new HashSet();
		s.add(field1);
		s.add(field2);
		assertAllPropertiesMappedExcept(s);
	}

	public void assertAllPropertiesMappedExcept(String field1)
			throws MappingException {
		Set s = new HashSet();
		s.add(field1);
		assertAllPropertiesMappedExcept(s);
	}

	public CompositeSetPropertyMapping getCompositeSetPropertyMapping(String propertyName) {
		Property property = classMapping.getProperty(propertyName);
		Value v = property.getValue();
		org.hibernate.mapping.Set value = (org.hibernate.mapping.Set) v;
		return new CompositeSetPropertyMapping(property, value, accessStrategy);
	}
}
